package game;

import static org.lwjgl.input.Keyboard.*;
import static org.lwjgl.input.Mouse.*;
import static org.lwjgl.opengl.GL11.*;

import java.util.Iterator;

import level.Level;
import model.Model;
import model.ModelFactory;
import netw.Receiver;
import netw.Transmitter;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.Display;
import org.lwjgl.util.vector.Vector3f;

import shader.ShaderProgram;
import entity.Camera;
import entity.Entity;
import entity.characters.Player;
import entity.characters.Character;
import entity.weapon.Shot;
import game.util.DisplayUtil;
import hud.Hud;

/**
 * @author Jevgeni Krutov
 * @author Sri Harsha Chilakapati
 */
public class Game {
	private static Game instance;//single instance allowed
	
	private static boolean IS_SERVER_MODE;
	
	/**
	 * an ideal fps value that should be maintained
	 */
	public static final int DEF_FPS = 60;
	
	/**
	 * minimal allowed fps for game logic to update correctly.
	 * Lower value of real fps means that the game will slow down
	 * in order to prevent CPU overload and avoid further slowdown.
	 */
	public static final int MIN_FPS = 5;
	
	/**
	 * how many ms have actually passed since the last frame
	 */
	private static long realDelta;
	
	/**
	 * how many ms the ideal frame should last for specified fps
	 */
	private static long constDelta = 1000 / DEF_FPS;
	
	/**
	 * bigger values of residueDelta will cause deliberate slowdown
	 */
	private static long maxResidueDelta = 1000 / MIN_FPS;
	
	/**
	 * delta leftover from previous frames; time debt to be compensated when possible
	 */
	private static long residueDelta = 0;
	

	String levelName;
	
	Camera camera;
	ShaderProgram shaderProgram;
	Vector3f absoluteLightPos = new Vector3f(-10, 5, -10);
	Level level;
	
	Player player;
	
	Entity skybox;
	Hud hud;
	
	public Game(String levelName, boolean isServerMode) {
		IS_SERVER_MODE = isServerMode;
		try {
			this.levelName = levelName;
			instance = this;
			DisplayUtil.setInstance(this);
			//Display.create(new PixelFormat(), new ContextAttribs(majorGL, minorGL).withForwardCompatible(true).withProfileCore(true));

			/* WORK CONFIG */
//			Display.create(); //work
//			DisplayUtil.setDisplayMode(250, 250); //work

			/* HOME CONFIG */
			Display.create(); //home
			DisplayUtil.setDisplayMode(800, 600); //home
//			DisplayUtil.setDisplayMode(1920, 1080, true); //home HD
			Display.setVSyncEnabled(true); //home
			
			Display.setResizable(true);
			gameLoop();
		} catch (LWJGLException e) {
			e.printStackTrace();
			System.exit(-1);
		}
	}

	private void gameLoop() {
		long lastFrame = getCurrentTime();
		long thisFrame = getCurrentTime();

		init();

		while (!Display.isCloseRequested()) {
			thisFrame = getCurrentTime();
			
			realDelta = thisFrame - lastFrame;
			residueDelta += realDelta;
			
			long updateTimes = residueDelta / constDelta;
			
			for(long i = 0; i < updateTimes + 1; i++) {
				update(constDelta);
				residueDelta -= constDelta;
				
				if(residueDelta > maxResidueDelta) {
					break;
				}
			}
			
			render();
			Display.update();

			if (Display.wasResized()) {
				DisplayUtil.resized();
				hud.setResolution(Display.getWidth(), Display.getHeight());
			}
			
			Display.sync(DEF_FPS);
			
			lastFrame = thisFrame;
		}
		end();
	}
	
	public void init() {
		level = new Level(levelName);
		level.createGround();
		level.createWalls();
		
		createPlayerAndHisCamera();
		
		level.createEnemies(player);
		level.createItems();
		
		createSkybox();
		createGui();

		setupMouse();
		createShaderProgram();
		setupDepthTestAndCulling();
		
		
		if(IS_SERVER_MODE) {
			receiver = new Receiver();
			receiver.start();
		} else {
			transmitter = new Transmitter(player);
			transmitter.start();
		}
	}

	private void createGui() {
		hud = new Hud(player, camera, level, Display.getWidth(), Display.getHeight());
	}
	
	private void createSkybox() {
		skybox = new Entity(new Vector3f(camera.getPosition().x, -1, camera.getPosition().z), new Vector3f(0, 0, 0));
		skybox.setModel(ModelFactory.getModelInstance("assets/skybox.obj"));
	}

	private void createPlayerAndHisCamera() {
		player = new Player(
				new Vector3f(level.getPlayerSpawnpoint().x, level.getPlayerSpawnpoint().y, level.getPlayerSpawnpoint().z),
				new Vector3f(0, 90, 0),
				ModelFactory.getModelInstance("assets/monkey.obj")
				);
		level.entities.add(player);
		
		camera = new Camera(
				new Vector3f(-6, 0, -8),
				new Vector3f(0, -135, 0),
				(float) Display.getWidth() / (float) Display.getHeight(),
				level
				);
		
		camera.setHostAndSetThisAsHisCamera((Player) player);
	}

	private void setupDepthTestAndCulling() {
		glEnable(GL_DEPTH_TEST);
		glDepthFunc(GL_LEQUAL);
		
		glEnable(GL_CULL_FACE);
		glCullFace(GL_BACK);
	}

	private void setupMouse() {
		//setGrabbed(true);
		setClipMouseCoordinatesToWindow(true);
	}

	private void createShaderProgram() {
		shaderProgram = new ShaderProgram();
		shaderProgram.attachVertexShader("shader/shader.vert");
		shaderProgram.attachFragmentShader("shader/shader.frag");
		shaderProgram.link();
	}

	/**
	 * Update the logic of the game.
	 * 
	 * @param elapsedTime
	 *            Time elapsed since last frame.
	 */
	public void update(long elapsedTime) {
		handleExitIfNeeded();
				
		decreaseLifetimesAndRemoveDeadEntities(elapsedTime);
				
		if(IS_SERVER_MODE) {
			player.setPosition(receiver.playerCoords.x, receiver.playerCoords.y, receiver.playerCoords.z);
			player.setRotation(receiver.playerCoords.rx, receiver.playerCoords.ry, receiver.playerCoords.rz);
		} else {
			//---
		}
		
		listenToActionsAndMoveIfNeeded(elapsedTime);
		listenToKeyboardAndHandleShooting(elapsedTime);
		
		updateSkyboxPosition();
		
		if(!IS_SERVER_MODE) {
			updatePhysics(elapsedTime);
		}
		
		camera.filter.updateTransitionIfNeeded(elapsedTime);
	}
	
	private void updateSkyboxPosition() {
		skybox.setPosition(camera.getPosition().x, -1, camera.getPosition().z);
	}

	private void listenToActionsAndMoveIfNeeded(long elapsedTime) {
		camera.listenToActionAndMove(elapsedTime);
		for(Entity e : level.entities) {
			e.listenToActionAndMove(elapsedTime);
		}
	}

	private void decreaseLifetimesAndRemoveDeadEntities(long elapsedTime) {
		Iterator<Entity> it = level.entities.iterator();
		while (it.hasNext()) {
		    Entity e = it.next(); 
		    e.decreaseAwaitingTimes(elapsedTime);
		    if(!e.isAlive()) {
		         it.remove();
		         level.entities.remove(e);
		    }
		}
	}

	public void render() {
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		// Perform camera transformations
		camera.apply();

		// Bind the shaders
		shaderProgram.bind();

		// Set the uniforms
		shaderProgram.setUniform("projection", camera.getProjectionMatrix());
		shaderProgram.setUniform("view", camera.getViewMatrix());
	
		shaderProgram.setUniform("filter", camera.filter.getCurrentFilter());
		shaderProgram.setUniform("filterTransition", camera.filter.getFilterTransition());
		
		absoluteLightPos = camera.getPosition();
		shaderProgram.setUniform("lightPos", absoluteLightPos);
		
		renderSkybox();
		
		// Render the model
		for(Entity e : level.entities) {
			if(!camera.isFreeFloatingMode() && e == camera.getHost()) {
				continue;//skip current player rendering
			}
			
			shaderProgram.setUniform("model", e.getMatrix());
			shaderProgram.setUniform("mNormal", camera.getNormalMatrix(e.getMatrix()));
			
			optimizeMatrices();				
			e.render();
		}

		// Unbind the shaders
		ShaderProgram.unbind();
		
		hud.drawHud();
	}
	
	private void renderSkybox() {
		shaderProgram.setUniform("model", skybox.getMatrix());
		shaderProgram.setUniform("mNormal", camera.getNormalMatrix(skybox.getMatrix()));
		skybox.render();
	}

	private void updatePhysics(long elapsedTime) {
		for(int i=0; i < level.entities.size(); i++) {
			
			Entity eI = level.entities.get(i);
			
			if(!eI.isShot())
				eI.applyForce(level.getGravity().getGravityVector());
			
			eI.changeSpeedAndMove(elapsedTime);
			
			for(int j = i + 1; j < level.entities.size(); j++) {
				
				Entity eJ = level.entities.get(j);
				
				if(!eI.isMovingEntity() && !eJ.isMovingEntity()) {
					//at least one in a pair should be MovingEntity to perform collision-test
					continue;
				}
				
				if(level.getCollisionDetector().detect(eI, eJ)) {
					//collision occurred
					level.getCollisionDetector().collide(eI, eJ);
				}
			}
		}
	}
	
	private void handleExitIfNeeded() {//TODO: handle differently
		while (Keyboard.next()) {
			if (getEventKey() == KEY_ESCAPE) {
				end();
			}
		}
		
		if(player.isWon()) {
			System.out.println("You win!");
			end();			
		}
		
		if(!player.isAlive()) {
			System.out.println("Game over!");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {}
			end();
		}
	}
	
	
	/* ------------------------------------------------------------------------------------ */
	
	/**
	 * @return Current time in milliseconds.
	 */
	public static long getCurrentTime() {
		return Sys.getTime() * 1000 / Sys.getTimerResolution();
	}
	
	private void listenToKeyboardAndHandleShooting(long elapsedTime) {
		for(int i = 0;; i++) {
			if(i == level.entities.size()) {
				break;
			}
			Entity e = level.entities.get(i);
			if(e.isCharacter()) {
				Character ch = (Character) e;
				if(ch.isShooting()) {
					Shot shot = ch.shoot(elapsedTime);
					if(shot != null) {
						level.entities.add(shot);
					}
				}
			}
		}
	}
	
	/**
	 * Dispose created resources.
	 */
	public void dispose() {
		for(Entity e : level.entities) {
			Model m = e.getModel();
			if(m != null) {
				m.dispose();
			}
		}
		shaderProgram.dispose();
	}
	
	/**
	 * Properly terminate the game.
	 */
	public static void end() {
		instance.dispose();
		instance = null;
		Display.destroy();
		System.exit(0);
	}
	
	private void optimizeMatrices() {
		//optimization 1: viewModel
//		shaderProgram.setUniform("viewModel", camera.getViewModel(e.getMatrix()));
		
		//optimization 2: projectionViewModel
//		Matrix4f projectionViewModel = new Matrix4f();
//		Matrix4f.mul(Matrix4f.mul(camera.getProjectionMatrix(), camera.getViewMatrix(), null), e.getMatrix(), projectionViewModel);
//		shaderProgram.setUniform("projectionViewModel", projectionViewModel);
	}
	
	
	
	// ---------------------------------- NETWORK STUFF: -------------------------------------------
	Receiver receiver;
	Transmitter transmitter;
}
